export NumberedLine
"""
    immutable NumberedLine

A line of code with an attached line number.

```jldoctest
julia> using NumberedLines

julia> NumberedLine(LineNumberNode(1), 1)
:( # line 1:
    1)
```
"""
struct NumberedLine
    number
    line
end

export is_line_number
"""
    is_line_number(a)

Test whether `a` is a line number (but not a [`NumberedLine`](@ref))

```jldoctest
julia> using NumberedLines

julia> is_line_number(1)
false

julia> node = LineNumberNode(1);

julia> is_line_number(node)
true

julia> is_line_number( NumberedLine(1, node))
false
```
"""
is_line_number(l::LineNumberNode) = true
is_line_number(e::Expr) = e.head == :line
is_line_number(e) = false

Base.show( io::IO, n::NumberedLine) =
    Base.show_unquoted_quote_expr(io, n, 0, -1)
Base.show_unquoted(io::IO, n::NumberedLine, index::Int, prec::Int) = begin
    Base.show_unquoted(io, n.number, index, prec)
    print(io, '\n', " "^index)
    Base.show_unquoted(io, n.line, index, prec)
end

export attach_line_number
"""
    attach_line_number(a, number)

Attach a `number` to `a`.

```jldoctest
julia> using NumberedLines

julia> test = attach_line_number(1, LineNumberNode(1) )
:( # line 1:
    1)

julia> attach_line_number(test, LineNumberNode(2) )
ERROR: Line already has line number
[...]
```

If `a` is a tuple or array, recur into the items.
```jldoctest
julia> using NumberedLines

julia> attach_line_number( [1, 2], LineNumberNode(1) )
2-element Array{NumberedLines.NumberedLine,1}:
 :( # line 1:
    1)
 :( # line 1:
    2)
```
"""
attach_line_number(n::NumberedLine, number) = error("Line already has line number")
attach_line_number(a, number) = NumberedLine(number, a)
attach_line_number(c::Union{Tuple, AbstractArray}, number) =
    map(c) do item
        attach_line_number(item, number)
    end

map_expression(f, e::Expr) = Expr(e.head, f.(e.args)...)

force_line_number(any, number) =
    attach_line_number(insert_line_number(any, number), number)
force_line_number(n::NumberedLine, number) =
    insert_line_number(n, number)

export insert_line_number
"""
    insert_line_number(any, number)

Will replace all line numbers in `any` with `number`.

```jldoctest
julia> using NumberedLines

julia> e = quote
            a = 1
            b = 2
        end;

julia> insert_line_number(attach_line_numbers(e), LineNumberNode(1))
quote
     # line 1:
    a = 1
     # line 1:
    b = 2
end
```

It will line numbers where there were none before:

```jldoctest
julia> using NumberedLines

julia> e = :(a = 1; b = 2);

julia> insert_line_number(e, LineNumberNode(1))
quote
     # line 1:
    a = 1
     # line 1:
    b = 2
end
```
"""
insert_line_number(any, number) = any
insert_line_number(n::NumberedLine, number) =
    attach_line_number(insert_line_number(n.line, number), number)
insert_line_number(e::Expr, number) =
    if e.head == :block
        map_expression(e) do arg
            force_line_number(arg, number)
        end
    else
        map_expression(e) do arg
            insert_line_number(arg, number)
        end
    end

attach_append!(unattached, attached) = begin
    if !isempty(unattached)
        maybe_number = shift!(unattached)
        if is_line_number(maybe_number)
            if isempty(unattached)
                push!(attached, NumberedLine(maybe_number, nothing) )
            else
                line = shift!(unattached) |> attach_line_numbers
                push!(attached, NumberedLine(maybe_number, line) )
            end
        else
            push!(attached, maybe_number |> attach_line_numbers)
        end
        attach_append!(unattached, attached)
    end
    attached
end

mutable struct LineCounter
    number::Int
end

tick!(line_counter) = begin
    line_counter.number = line_counter.number + 1
    LineNumberNode(line_counter.number)
end

pretty_number!(any, line_counter) = any
pretty_number!(n::NumberedLine, line_counter) =
    NumberedLine(tick!(line_counter), pretty_number!(n.line, line_counter) )
pretty_number!(e::Expr, line_counter) = map_expression(e) do arg
    pretty_number!(arg, line_counter)
end
pretty_number(e) = pretty_number!(e, LineCounter(0))

export attach_line_numbers
"""
    attach_line_numbers(a)

Attach line numbers to lines

```jldoctest
julia> using NumberedLines

julia> e = quote
           a = 1
           if a == 1
               2
           end
       end |> attach_line_numbers;

julia> NumberedLines.pretty_number(e) # renumber for reproducibility
quote
     # line 1:
    a = 1
     # line 2:
    if a == 1
         # line 3:
        2
    end
end
```

Already attached line numbers will be ignored.

```jldoctest
julia> using NumberedLines

julia> e_attached = quote
           1
       end |> attach_line_numbers;

julia> e_attached == attach_line_numbers(e_attached)
true
```
"""
attach_line_numbers(a) = a
attach_line_numbers(n::NumberedLine) =
    without_line_number(attach_line_numbers, n)
attach_line_numbers(e::Expr) = begin
    result = Expr(e.head)
    attach_append!(copy(e.args), result.args)
    result
end

detach_append!(v, a) = push!(v, detach_line_numbers(a) )
detach_append!(v, n::NumberedLine) =
    push!(v, n.number, detach_line_numbers( n.line) )

export detach_line_numbers
"""
    detach_line_numbers(a)

Recursively detach line numbers from lines

```jldoctest
julia> using NumberedLines

julia> e = quote
           a = 1
           if a == 1
               2
           end
       end;

julia> e |> attach_line_numbers |> detach_line_numbers == e
true
```
"""
detach_line_numbers(a) = a
detach_line_numbers(e::Expr) = begin
    result = Expr(e.head)
    foreach(arg -> detach_append!(result.args, arg), e.args)
    result
end

export without_line_number
"""
    without_line_number(f, n::NumberedLine)

Apply `f` to `n.line` and attach ([`attach_line_number`](@ref)) `n.number` to
the result

```jldoctest
julia> using NumberedLines

julia> add_one(a) = :(\$a + 1);

julia> without_line_number(add_one, 1)
:(1 + 1)

julia> n = NumberedLine(LineNumberNode(1), 1)
:( # line 1:
    1)

julia> without_line_number(add_one, n)
:( # line 1:
    1 + 1)
```
"""
without_line_number(f, n::NumberedLine) = attach_line_number(f(n.line), n.number)
without_line_number(f, a) = f(a)

export with_numbered_lines
"""
    with_numbered_lines(f, e)

[`attach_line_numbers`](@ref) to `e`, apply `f`, then
[`detach_line_numbers`](@ref)

```jldoctest
julia> using NumberedLines

julia> reverse_lines(e) = Expr(e.head, e.args[2], e.args[1]);

julia> e = quote
           1
           2
       end;

julia> result = with_numbered_lines(reverse_lines, e);

julia> (2, 1) == ( result.args[2], result.args[4] )
true
```
"""
with_numbered_lines(f, e) = e |> attach_line_numbers |> f |> detach_line_numbers

export wrap
"""
    wrap(a)

Will wrap a [`NumberedLine`](@ref) inside of a begin block. This can be
important because in julia syntax trees, line numbers must be directly inside
begin blocks.

```jldoctest
julia> using NumberedLines

julia> wrap(1)
1

julia> NumberedLine(LineNumberNode(1), 1) |> wrap
quote
     # line 1:
    1
end
```
"""
wrap(a) = a
wrap(n::NumberedLine) = Expr(:block, n)

import Base.==

==(n1::NumberedLine, n2::NumberedLine) =
    n2.number == n2.number &&
    n1.line == n2.line
